/*
 * File     : StructureHelper.java
 * Created  : 19 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.structure;

import static com.googlecode.dni.internal.DniInternal.LOGGER;
import static com.googlecode.dni.internal.DniInternal.MEMORY_ACCESSOR;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.ShortBuffer;
import java.util.logging.Level;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.type.FreeableNativeObject;
import com.googlecode.dni.type.NativeStringBuffer;
import com.googlecode.dni.type.Pointer;


/**
 * <p>
 *  Provides helper methods for structure implementations.
 * </p>
 *
 * @author Matthew Wilson
 */
public final class StructureHelper
{

    /** Prevents instantiation. */
    private StructureHelper()
    {
        throw new UnsupportedOperationException();
    }

    /**
     * @param a
     *            the first object
     * @param b
     *            the second object
     * @return <code>true</code> iff the objects are equal
     */
    public static boolean equals( final Object a, final Object b )
    {
        if ( a == b )
        {
            return true;
        }

        return a == null ? false : a.equals( b );
    }

    /**
     * @param a
     *            the first value
     * @param b
     *            the second value
     * @return <code>true</code> iff the values are equal
     */
    public static boolean equals( final float a, final float b )
    {
        return Float.floatToRawIntBits( a ) == Float.floatToRawIntBits( b );
    }

    /**
     * @param a
     *            the first value
     * @param b
     *            the second value
     * @return <code>true</code> iff the values are equal
     */
    public static boolean equals( final double a, final double b )
    {
        return Double.doubleToRawLongBits( a ) == Double.doubleToRawLongBits( b );
    }

    /**
     * @param a
     *            the first value
     * @param b
     *            the second value
     * @return <code>true</code> iff the values are equal
     */
    public static boolean equals( final long a, final long b )
    {
        return a == b;
    }

    /**
     * @param value
     *            the value
     * @return <code>true</code> iff the value is zero
     */
    public static boolean isZero( final long value )
    {
        return value == 0L;
    }

    /**
     * @param object
     *            the object
     * @return a hash-code
     */
    public static int hashCode( final Object object )
    {
        return object != null ? object.hashCode() : 0;
    }

    /**
     * @param value
     *            the value
     * @return a hash-code
     */
    public static int hashCode( final long value )
    {
        return ( (int) value ) ^ ( (int) ( value >> 32 ) );
    }

    /**
     * @param value
     *            the value
     * @return a hash-code
     */
    public static int hashCode( final float value )
    {
        return Float.floatToRawIntBits( value );
    }

    /**
     * @param value
     *            the value
     * @return a hash-code
     */
    public static int hashCode( final double value )
    {
        return hashCode( Double.doubleToRawLongBits( value ) );
    }

    /**
     * <p>
     *  Obtains the address of the given structure.
     * </p>
     *
     * @param structure
     *            the structure, or <code>null</code>
     *
     * @return the address, or zero is the structure was <code>null</code>
     */
    public static long getAddress( final AbstractStructure structure )
    {
        return structure != null ? structure.pointer : 0;
    }

    /**
     * Updates a buffer according to the rules for structures.
     *
     * @param buffer
     *            the buffer
     * @param pointer
     *            the pointer
     * @return <code>true</code> iff updated
     */
    public static boolean updateBuffer( final ByteBuffer buffer, final long pointer )
    {
        long offset = pointer - Pointer.fromBuffer( buffer ).address();
        if ( offset < 0 || offset > buffer.capacity() )
        {
            return false;
        }

        int iOffset = (int) offset;
        if ( iOffset > buffer.limit() )
        {
            buffer.limit( iOffset );
        }
        buffer.position( iOffset );
        return true;
    }

    /**
     * Updates a buffer according to the rules for structures.
     *
     * @param buffer
     *            the buffer
     * @param pointer
     *            the pointer
     * @return <code>true</code> iff updated
     */
    public static boolean updateBuffer( final ShortBuffer buffer, final long pointer )
    {
        return updateBufferImpl( buffer, pointer, 2 );
    }

    /**
     * Updates a buffer according to the rules for structures.
     *
     * @param buffer
     *            the buffer
     * @param pointer
     *            the pointer
     * @return <code>true</code> iff updated
     */
    public static boolean updateBuffer( final CharBuffer buffer, final long pointer )
    {
        return updateBufferImpl( buffer, pointer, 2 );
    }

    /**
     * Updates a buffer according to the rules for structures.
     *
     * @param buffer
     *            the buffer
     * @param pointer
     *            the pointer
     * @return <code>true</code> iff updated
     */
    public static boolean updateBuffer( final IntBuffer buffer, final long pointer )
    {
        return updateBufferImpl( buffer, pointer, 4 );
    }

    /**
     * Updates a buffer according to the rules for structures.
     *
     * @param buffer
     *            the buffer
     * @param pointer
     *            the pointer
     * @return <code>true</code> iff updated
     */
    public static boolean updateBuffer( final LongBuffer buffer, final long pointer )
    {
        return updateBufferImpl( buffer, pointer, 8 );
    }

    /**
     * Updates a buffer according to the rules for structures.
     *
     * @param buffer
     *            the buffer
     * @param pointer
     *            the pointer
     * @return <code>true</code> iff updated
     */
    public static boolean updateBuffer( final FloatBuffer buffer, final long pointer )
    {
        return updateBufferImpl( buffer, pointer, 4 );
    }

    /**
     * Updates a buffer according to the rules for structures.
     *
     * @param buffer
     *            the buffer
     * @param pointer
     *            the pointer
     * @return <code>true</code> iff updated
     */
    public static boolean updateBuffer( final DoubleBuffer buffer, final long pointer )
    {
        return updateBufferImpl( buffer, pointer, 8 );
    }

    /**
     * @param pointer
     *            the pointer
     * @param size
     *            the size
     * @return a buffer
     */
    public static ByteBuffer getSizedByteBuffer( final long pointer, final int size )
    {
        return DniInternal.MEMORY_ACCESSOR.getBuffer( pointer, size );
    }

    /**
     * @param pointer
     *            the pointer
     * @param size
     *            the size
     * @return a buffer
     */
    public static ShortBuffer getSizedShortBuffer( final long pointer, final int size )
    {
        if ( pointer == 0L )
        {
            return null;
        }

        return DniInternal.MEMORY_ACCESSOR.getBuffer( pointer, size ).asShortBuffer();
    }

    /**
     * @param pointer
     *            the pointer
     * @param size
     *            the size
     * @return a buffer
     */
    public static CharBuffer getSizedCharBuffer( final long pointer, final int size )
    {
        if ( pointer == 0L )
        {
            return null;
        }

        return DniInternal.MEMORY_ACCESSOR.getBuffer( pointer, size ).asCharBuffer();
    }

    /**
     * @param pointer
     *            the pointer
     * @param size
     *            the size
     * @return a buffer
     */
    public static IntBuffer getSizedIntBuffer( final long pointer, final int size )
    {
        if ( pointer == 0L )
        {
            return null;
        }

        return DniInternal.MEMORY_ACCESSOR.getBuffer( pointer, size ).asIntBuffer();
    }

    /**
     * @param pointer
     *            the pointer
     * @param size
     *            the size
     * @return a buffer
     */
    public static LongBuffer getSizedLongBuffer( final long pointer, final int size )
    {
        if ( pointer == 0L )
        {
            return null;
        }

        return DniInternal.MEMORY_ACCESSOR.getBuffer( pointer, size ).asLongBuffer();
    }

    /**
     * @param pointer
     *            the pointer
     * @param size
     *            the size
     * @return a buffer
     */
    public static FloatBuffer getSizedFloatBuffer( final long pointer, final int size )
    {
        if ( pointer == 0L )
        {
            return null;
        }

        return DniInternal.MEMORY_ACCESSOR.getBuffer( pointer, size ).asFloatBuffer();
    }

    /**
     * @param pointer
     *            the pointer
     * @param size
     *            the size
     * @return a buffer
     */
    public static DoubleBuffer getSizedDoubleBuffer( final long pointer, final int size )
    {
        if ( pointer == 0L )
        {
            return null;
        }

        return DniInternal.MEMORY_ACCESSOR.getBuffer( pointer, size ).asDoubleBuffer();
    }

    /**
     * Handles an exception thrown in a finalizer of a structure.
     *
     * @param structure
     *            the structure
     * @param throwable
     *            the throwable
     */
    public static void handleFinalizerThrowable( final Object structure,
                                                 final Throwable throwable )
    {
        LOGGER.log( Level.WARNING, "Failed to finalize structure: " + structure, throwable );
    }

    /**
     * @param buffer
     *            the buffer
     * @param requiredSize
     *            the required size
     *
     * @throws IllegalArgumentException
     *            if the buffer is too small
     */
    public static void checkFixedBufferSize( final Buffer buffer, final int requiredSize )
    {
        if ( buffer.remaining() < requiredSize )
        {
            throw new IllegalArgumentException( "Buffer too small; required = " + requiredSize + "; remaining = " + buffer.remaining() );
        }
    }

    /**
     * @param buffer
     *            the buffer
     * @param requiredSize
     *            the required size
     *
     * @throws IllegalArgumentException
     *            if the buffer is too small
     */
    public static void checkDynamicBufferSize( final Buffer buffer, final int requiredSize )
    {
        if ( requiredSize < 0 )
        {
            throw new IllegalArgumentException( "Required size is negative: " + requiredSize );
        }
        if ( buffer.remaining() < requiredSize )
        {
            throw new IllegalArgumentException( "Buffer too small; required = " + requiredSize + "; remaining = " + buffer.remaining() );
        }
    }

    /**
     * Checks the given int value as a size for a buffer.
     *
     * @param size
     *            the size
     * @return the size
     * @throws IllegalStateException
     *            if the size is too big
     */
    public static int checkIntAsSize( final int size )
    {
        if ( size < 0 )
        {
            throw badSize( size );
        }

        return size;
    }

    /**
     * Checks the given int value as a size for a buffer when the value has to
     * be scaled.
     *
     * @param size
     *            the size, in units
     * @param unitShift
     *            the unit shift (that is, unit size is
     *            <code>1 &lt;&lt; unitShift</code>)
     * @return the size, in bytes
     * @throws IllegalStateException
     *            if the size is too big
     */
    public static int checkIntAsSize( final int size, final int unitShift )
    {
        if ( size < 0 || size > ( Integer.MAX_VALUE >>> unitShift ) )
        {
            throw badIntAsSize( size, unitShift );
        }

        return size << unitShift;
    }

    /**
     * Checks the given long value as a size for a buffer.
     *
     * @param size
     *            the size
     * @return the size
     * @throws IllegalStateException
     *            if the size is too big
     */
    public static int checkLongAsSize( final long size )
    {
        // whether unsigned or signed, a 'big' long is out of range
        if ( size < 0 || size > Integer.MAX_VALUE )
        {
            throw badLongAsSize( size );
        }

        return (int) size;
    }

    /**
     * Checks the given long value as a size for a buffer.
     *
     * @param size
     *            the size
     * @param unitSize
     *            the unit size
     * @return the size
     * @throws IllegalStateException
     *            if the size is too big
     */
    public static int checkLongAsSize( final long size, final int unitSize )
    {
        // whether unsigned or signed, a 'big' long is out of range
        if ( size < 0 || size > Integer.MAX_VALUE || size * unitSize > Integer.MAX_VALUE )
        {
            throw badLongAsSize( size );
        }

        return (int) size;
    }

    /**
     * Checks the given long value as a size for a buffer.
     *
     * @param size
     *            the size
     * @param unitSize
     *            the unit size
     * @return the size, or -1 if too big
     */
    public static int fixLongAsSize( final long size, final int unitSize )
    {
        // whether unsigned or signed, a 'big' long is out of range
        if ( size > Integer.MAX_VALUE || size * unitSize > Integer.MAX_VALUE )
        {
            return -1;
        }

        return (int) size;
    }

    /**
     * Determines if the value is not <code>null</code>.
     *
     * @param object
     *            the object value
     * @return <code>true</code> iff not null
     */
    public static boolean isNotNull( final Object object )
    {
        return object != null;
    }

    /**
     * Throws when a {@link FreeableNativeObject} structure cannot be freed
     * because it is owned by something else.
     *
     * @param structure
     *            the structure
     * @param owner
     *            the owner
     *
     * @throws IllegalStateException
     *             always
     */
    public static void throwCannotFree( final Object structure,
                                        final Object owner )
    {
        throw new IllegalStateException( "Cannot explicitly free structure: " + structure
                                         + " (owned by " + owner + ")" );
    }

    /**
     * Checks the buffer's capacity.
     *
     * @param buffer
     *            the buffer
     * @param expectedCapacity
     *            the expected capacity
     */
    public static void checkStringBufferSize( final NativeStringBuffer buffer,
                                              final int expectedCapacity )
    {
        if ( buffer.capacity() != expectedCapacity )
        {
            throw badStringBufferSize( buffer, expectedCapacity );
        }
    }

    /**
     * Allocates a structure and copies 8 bytes of the given value.
     *
     * @param factory
     *            the factory
     * @param value
     *            the value
     * @return a structure
     */
    public static AbstractStructure allocateAndCopy8( final AbstractStructureFactory< ? > factory,
                                                      final long value )
    {
        AbstractStructure structure = factory.allocateUntyped();
        MEMORY_ACCESSOR.putLong( structure.pointer, value );
        return structure;
    }

    /**
     * Updates a buffer according to the rules for structures.
     *
     * @param buffer
     *            the buffer
     * @param pointer
     *            the pointer
     * @param unitSize
     *            the primitive unit size
     * @return <code>true</code> iff updated
     */
    private static boolean updateBufferImpl( final Buffer buffer,
                                             final long pointer,
                                             final int unitSize )
    {
        long offset = pointer - DniInternal.MEMORY_ACCESSOR.getBufferPointer( buffer );
        if ( offset < 0 || offset > buffer.capacity() * (long) unitSize || ( offset % unitSize ) != 0 )
        {
            return false;
        }

        int iOffset = (int) ( offset / unitSize );
        if ( iOffset > buffer.limit() )
        {
            buffer.limit( iOffset );
        }
        buffer.position( iOffset );
        return true;
    }

    private static IllegalStateException badIntAsSize( final long size, final int unitShift )
    {
        return new IllegalStateException( "Bad size (unit size " + ( 1 << unitShift ) + "): " + size );
    }

    private static IllegalStateException badLongAsSize( final long size )
    {
        return new IllegalStateException( "Bad size (long): " + size );
    }

    private static IllegalArgumentException badStringBufferSize( final NativeStringBuffer buffer,
                                                                 final int expectedCapacity )
    {
        return new IllegalArgumentException( "Bad capacity: was " + buffer.capacity() + " but expected " + expectedCapacity );
    }

    private static IllegalStateException badSize( final int size )
    {
        return new IllegalStateException( "Size is negative: " + size );
    }

}
